Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 6 Vererbung, Polymorphie und Schnittstellen
  gp 6.1 Basisklassen und abgeleitete Klassen
    gp 6.1.1 Ableiten einer Klasse
    gp 6.1.2 Klassen, die nicht vererben können
    gp 6.1.3 Zusammenfassung
  gp 6.2 Konstruktoren in abgeleiteten Klassen
    gp 6.2.1 Die Konstruktoren der Klasse »GraphicCircle«
    gp 6.2.2 Der Zugriffsmodifizierer »protected«
    gp 6.2.3 Konstruktorverkettung
    gp 6.2.4 Destruktor-Verkettung
    gp 6.2.5 Der Stand des Projekts »CircleApplication«
    gp 6.2.6 Zusammenfassung
  gp 6.3 Die Methoden in einer abgeleiteten Klasse
    gp 6.3.1 Geerbte Methoden mit »new« verdecken
    gp 6.3.2 Überladen einer Basisklassenmethode
  gp 6.4 Ereignisse in der Vererbung
  gp 6.5 »Hat-eine«-Beziehungen (Aggregation)
    gp 6.5.1 Weiterleitung einer internen Objektreferenz
    gp 6.5.2 Verbergen des internen Objekts
    gp 6.5.3 Innere Klassen
  gp 6.6 Typumwandlung von Objektvariablen
    gp 6.6.1 Die implizite Typumwandlung von Objektreferenzen
    gp 6.6.2 Die explizite Typumwandlung von Objektreferenzen
    gp 6.6.3 Zusammenfassung
  gp 6.7 Abstrakte Klassen und Methoden
    gp 6.7.1 Abstrakte Definitionen
  gp 6.8 Polymorphie
    gp 6.8.1 Virtuelle Methoden
    gp 6.8.2 Inhomogene Mengen
    gp 6.8.3 Verdecken und Überschreiben geerbter Methoden
    gp 6.8.4 Überschreiben der Methode »ToString()« der Klasse »Object«
    gp 6.8.5 Versiegelte Methoden
    gp 6.8.6 Zusammenfassung
  gp 6.9 Erweiterung der Klassenhierarchie »CircleApplication«
    gp 6.9.1 Die Klasse »GeometricObject«
  gp 6.10 Schnittstellen
    gp 6.10.1 Einführung in die Schnittstellen
    gp 6.10.2 Schnittstellendefinition
    gp 6.10.3 Schnittstellenimplementierung
    gp 6.10.4 Typumwandlung mit dem »as«-Operator
    gp 6.10.5 Abstrakte Klassen vs. Schnittstellen
    gp 6.10.6 Zusammenfassung


Galileo Computing

6.10 Schnittstellen  downtop

Das Konzept der Schnittstellen ist am einfachsten zu verstehen, wenn man sich deutlich macht, worin genau der Unterschied zwischen einer Klasse und einem Objekt besteht.


Galileo Computing

6.10.1 Einführung in die Schnittstellen  downtop

Klassen sind Schablonen, in denen Methoden und Eigenschaften definiert sind. Die Methoden manipulieren die Eigenschaften und stellen damit das Verhalten eines Objekts sicher. Ein Objekt wird jedoch nicht durch sein Verhalten, sondern durch seine Daten beschrieben, die über Eigenschaften manipuliert werden.

Treiben wir die Abstraktion noch weiter. Wenn sich ein Objekt durch Daten beschreibt und in einer Klassendefinition Eigenschaften und Methoden definiert sind, dann muss es auch ein Extrem geben, das nur Verhaltensweisen festlegt: Und genau das sind die Schnittstellen.

Die Aufgabe der Schnittstellen geht über die einfache Fähigkeit, Verhaltensweisen bereitzustellen, hinaus. Bekanntlich unterstützt die Common Language Runtime keine Mehrfachvererbung. Damit sind die .NET-Architekten möglichen Schwierigkeiten aus dem Weg gegangen, die mit der Mehrfachvererbung verbunden sind. Mehrfachvererbung ist nur schwer zu realisieren und wird deshalb in der Praxis auch nur selten eingesetzt. Andererseits hielt man es als für erstrebenswert, neben der Basisklasse weitere »Oberbegriffe« zuzulassen, um gemeinsame Merkmale mehrerer ansonsten unabhängiger Klassen beschreiben zu können. Mit der Schnittstelle wurde ein Konstrukt geschaffen, das genau diese Möglichkeiten bietet.

Sie müssen sich Schnittstellen wie eine Vertragsvereinbarung vorstellen. Sobald eine Klasse eine Schnittstelle implementiert, haben alle auf die Klasse zugreifenden Clients die Garantie, dass die Klasse alle Verhaltensdefinitionen der Schnittstelle veröffentlicht. Mit anderen Worten: Eine Schnittstelle legt einen Vertragsrahmen fest, den die implementierende Klasse erfüllen muss.


Galileo Computing

6.10.2 Schnittstellendefinition  downtop

Schnittstellen können Methoden, Eigenschaften und Indexer implementieren (Indexer werden im nächsten Kapitel behandelt). Das Besondere an einer Schnittstelle ist, dass sie selbst keine Codeimplementierung enthält, sondern ausnahmslos nur abstrakte Definitionen. Schauen wir uns dazu ein einfaches Beispiel an:


public interface ILuftfahrzeug {
  string Hersteller {get; set;}
  void Starten();
  void Landen();
}

Die Definition einer Schnittstelle ähnelt der Definition einer Klasse, bei der das Schlüsselwort class gegen das Schlüsselwort interface ausgetauscht worden ist. Fehlt die Angabe eines Zugriffsmodifizierers, gilt eine Schnittstelle standardmäßig als internal, ansonsten bietet sich noch public an. Deklarieren Sie eine Schnittstelle innerhalb eines Typs (also einer Klasse), kommen noch protected und private hinzu. In der Praxis sind innere Interfaces aber bedeutungslos. Hinter der Definition werden in geschweiften Klammern alle Mitglieder der Schnittstelle aufgeführt, die durch ein Semikolon voneinander getrennt werden. Beachten Sie, dass das von den abstrakten Klassen her bekannte Schlüsselwort abstract in einer Schnittstellendefinition nicht auftaucht.


Hinweis   Konventionsgemäß wird dem Bezeichner einer Schnittstelle ein »I« vorangestellt.

Die Schnittstelle ILuftfahrzeug implementiert die Eigenschaft Hersteller sowie die Methoden Starten und Landen. Weil eine Schnittstelle grundsätzlich nur abstrakte Definitionen bereitstellt, enthält kein Mitglied einen Anweisungsblock. Es ist auch kein Zugriffsmodifizierer angegeben, weil eine Schnittstellendefinition das nicht erlaubt. Der Compiler würde mit einer Fehlermeldung reagieren, wenn Sie einem Schnittstellenmitglied einen Zugriffsmodifizierer voranstellen, denn die Festlegung eines Zugriffsmodifizierers ist die Aufgabe der implementierenden Klasse.

Schnittstellen definieren einzelne oder auch eine Gruppe zusammengehöriger Member. Damit wird eine Spezifikation beschrieben, an die sich die implementierende Klasse halten muss: Sie verpflichtet sich, alle Elemente der Schnittstelle zu übernehmen. Auf eine Schnittstelle sind Sie bereits in Kapitel 4 gestoßen: Es war IDisposable. Klassen, die dieses Interface implementieren, garantieren damit, eine Methode Dispose bereitzustellen.


Galileo Computing

6.10.3 Schnittstellenimplementierung  downtop

Bei der Vererbung wird von »Ableitung« gesprochen, analog wurde bei den Schnittstellen der Begriff »Implementierung« geprägt. Eine Schnittstelle ist wie ein Vertrag, den eine Klasse unterschreibt, sobald sie eine bestimmte Schnittstelle implementiert. Das hat Konsequenzen: Eine Klasse, die eine Schnittstelle implementiert, muss ausnahmslos jedes Mitglied der Schnittstelle übernehmen.

Eine zu implementierende Schnittstelle wird, getrennt durch einen Doppelpunkt, hinter dem Klassenbezeichner angegeben:


class Hubschrauber : ILuftfahrzeug {...}

Eine Klasse ist nicht nur auf die Implementierung einer Schnittstelle beschränkt, es dürfen – im Gegensatz zur Vererbung – auch mehrere sein. Wird die Klasse außerdem noch aus einer anderen Klasse abgeleitet oder implementiert die Klasse mehrere Schnittstellen, werden alle Typbezeichner durch ein Komma getrennt aufgelistet:


class Hubschrauber : ILuftfahrzeug, IDisposable  {...}

Schnittstellen dürfen nach der Veröffentlichung nicht mehr verändert werden, da sowohl der Client als auch die implementierende Klasse in einem Vertragsverhältnis zueinander stehen und die Bedingungen des Vertrags erfüllt werden müssen. Im wirklichen Leben ist das auch nicht anders. Mit der Veröffentlichung einer Schnittstelle erklärt sich eine Klasse bereit, die Schnittstelle exakt so zu implementieren, wie sie entworfen wurde. Die von der Klasse übernommenen Mitglieder der Schnittstelle müssen daher in jeder Hinsicht identisch zu ihrer Definition sein:

gp  Der Name muss dem in der Schnittstelle entsprechen.
gp  Rückgabewert und Parameterliste dürfen nicht von denen in der Schnittstellendefinition abweichen.

Ein aus einer Schnittstelle übernommenes Mitglied darf nur public sein. Es ist zulässig, in einer Klasse ein Schnittstellenmitglied abstract oder virtual zu implementieren, es darf jedoch nicht static oder const sein.

Das folgende Codefragment zeigt die Klasse Hubschrauber, welche die oben definierte Schnittstelle ILuftfahrzeug implementiert und konventionsgemäß alle Schnittstellenmitglieder veröffentlicht:


public class Hubschrauber : ILuftfahrzeug {
  public void Starten() {
    // Anweisungen
  }
  public void Landen() {
    // Anweisungen
  }
  public string Hersteller {
    get {
      // Anweisungen
    }
    set {
      // Anweisungen
    }
  }
}

Schnittstellen und Vererbung

Vererbung ist eines der Kernkonzepte objektorientierter Systeme. Eine Klasse, die aus einer anderen Klasse abgeleitet wird, erbt alle Methoden der Basisklasse. Wir wissen auch, dass Ereignisse aus der Basisklasse nicht an die abgeleitete Klasse vererbt werden. Daher stellt sich die Frage, ob ein Interface gleichermaßen ein nicht vererbbares Feature darstellt oder die abgeleitete Klasse die aus einer Schnittstelle übernommenen Methoden der Basisklasse veröffentlicht. Wir wollen das an einem kleinen Beispiel prüfen.


interface IMyInterface {
  void Proc();
}
class ClassA : IMyInterface {
  public void Proc() {
    Console.WriteLine("Proc in ClassA");
  }
}
class ClassB : ClassA {
  // Anweisungen
}
class Program {
  static void Main(string[] args) {
    ClassB obj = new ClassB();
    obj.Proc();
    Console.ReadLine();
  }
}

Die Schnittstelle IMyInterface definiert die Methode Proc, die von jeder implementierenden Klasse übernommen werden muss – im Code oben ist es die Klasse ClassA. ClassB wird aus ClassA abgeleitet. Wenn Sie dieses sehr einfache Programm starten, wird es fehlerfrei ausgeführt und beweist damit, dass auch die Schnittstellenmethoden vererbt werden.

Hinsichtlich einer Schnittstelle zeigt eine Methode polymorphes Verhalten. Das setzt sich jedoch nicht bei den ableitenden Klassen durch. Eine ableitende Klasse kann daher die implementierte Methode nur erben oder mit new verdecken. Soll sich die Methode auch in den ableitenden Klassen polymorph verhalten, muss sie mit dem Modifizierer virtual signiert werden.

Mehrdeutigkeiten mit expliziter Implementierung vermeiden

Implementiert eine Klasse mehrere Schnittstellen, kann es passieren, dass in zwei oder mehr Schnittstellen ein gleichnamiges Mitglied definiert ist. Diese Mehrdeutigkeit wird durch die explizite Implementierung eines Schnittstellenmembers aus der Welt geschafft. Eine explizite Implementierung ist der vollständig kennzeichnende Name eines Schnittstellenmitglieds, bestehend aus dem Namen der Schnittstelle und dem Bezeichner des implementierten Mitglieds, getrennt durch einen Punkt.

Nehmen wir an, in den beiden Schnittstellen ICopy und IDocument wäre jeweils eine Methode Copy definiert:


// Interface-Definitionen
public interface ICopy {
  void Copy();
}
public interface IDocument {
  void Copy();
}

In einer Klasse ClassA, die beide Schnittstelle veröffentlicht, könnten die Methoden folgendermaßen explizit implementiert werden, um sie eindeutig den Schnittstellen zuzuordnen:


Class ClassA : ICopy, IDocument{
  void ICopy.Copy() {
    Console.WriteLine("Copy-Methode in ICopy");
  }
  void IDocument.Copy() {
    Console.WriteLine("Copy-Methode in IDocument");
  }
}

Es müssen nicht zwangsläufig beide Methoden explizit implementiert werden. Um eine eindeutige Schnittstellenzuordnung zu gewährleisten, würde eine explizite Implementierung vollkommen ausreichen. Explizit implementierte Methoden haben keinen Zugriffsmodifizierer, denn im Zusammenhang mit der expliziten Schnittstellenimplementierung ist eine wichtige Regel zu beachten:


Bei der expliziten Implementierung eines Schnittstellenmembers darf weder ein Zugriffsmodifizierer noch einer der Modifikatoren abstract, virtual, override oder static angegeben werden.

Auf die explizite Implementierung eines Schnittstellenmembers kann nur über eine Schnittstelleninstanz zugegriffen werden, der die Referenz auf das konkrete Objekt zugewiesen wird, dessen Typdefinition das Schnittstellenmember explizit implementiert.


class ClassA : ICopy, IDocument {
  static void Main(string[] args) {
    ClassA obj = new ClassA();
    ICopy myCopy = obj;
    myCopy.Copy();
    IDocument myDocu = obj;
    myDocu.Copy();
  }
  void ICopy.Copy() {
    Console.WriteLine("ICopy.Copy in ClassA");
  }
  void IDocument.Copy() {
    Console.WriteLine("IDocument.Copy in ClassA");
  }
}

Einer Objektvariablen kann nur dann eine Referenz zugewiesen werden, wenn sich beide in einer Vererbungsbeziehung befinden, sich eine Klasse somit aus einer anderen ableitet. Eine implementierte Schnittstelle wird wie eine Basisklasse behandelt. Deshalb kann einer Schnittstellenreferenz die Referenz auf ein Objekt vom Typ der implementierenden Klasse zugewiesen werden. Es kommt zu einer impliziten Konvertierung:


ICopy myCopy = obj;

Im Anschluss daran wird auf die Schnittstellenreferenz die Methode Copy aufgerufen:


myCopy.Copy();

Eine in einer Schnittstelle definierte Methode ist, wie auch eine abstrakte Methode, implizit virtuell. Die Bindung erfolgt dynamisch zur Laufzeit, der Aufruf ist polymorph und wird an die Methode des tatsächlichen Typs weitergeleitet. An der Konsole erscheint die Ausgabe


ICopy.Copy in ClassA
IDocument.Copy in ClassA

Explizite Implementierungen von Schnittstellenmitgliedern heben Mehrdeutigkeiten bei Schnittstellenmitgliedern mit derselben Signatur auf. Ohne explizite Implementierung könnte eine Klasse keine gleichnamigen Schnittstellenmethoden mit derselben Signatur haben. Es gibt noch einen zweiten Grund, sich für die explizite Implementierung zu entscheiden: Über einen Objektverweis kann nicht auf explizite Implementierungen zugegriffen werden. Es ist so, als wäre das implementierte Schnittstellenmitglied private definiert, der direkte Zugriff ist nicht möglich.

Schnittstellen, die selbst Schnittstellen implementieren

Mehrere Schnittstellen können zu einer neuen Schnittstelle zusammengefasst werden. Das folgende Codefragment zeigt, wie die Schnittstelle ICollect die beiden Schnittstellen IInterface1 und IInterface2 beerbt.


interface IInterface1 {
  void Proc1();
}
interface IInterface2 : IInterface1 {
  void Proc2();
}
interface ICollect : IInterface1, IInterface2 {
  void Proc3();
}

Eine Klasse, die sich die Dienste der Schnittstellen IInterface1, IInterface2 und ICollect sichern möchte, braucht dazu nur die Schnittstelle ICollect zu implementieren:


class ClassA : ICollect {
  public void Proc3() {
    // Anweisungen 
  }
  public void Proc1() {
    // Anweisungen 
  }
  public void Proc2() {
    // Anweisungen 
  }
}

Hat eine Klasse eine bestimmte Schnittstelle implementiert?

In der täglichen Programmierpraxis werden Sie immer wieder auf dieselben Schwierigkeiten stoßen und Lösungen entwerfen müssen. Eine der aufgeworfenen Fragen wird lauten: Wie kann ich feststellen, ob der Typ eines Objekts eine bestimmte Schnittstelle implementiert?

Betrachten wir dazu ein einfaches Beispiel und stellen uns vor, dass in der Anwendung CircleApplication die Schnittstelle IDraw definiert ist, die von den Klassen GraphicCircle und GraphicRectangle implementiert wird.


// ------------------------------------------------------------
// Beispiel: ...\Kapitel 6\Schnittstellenprüfung
// ------------------------------------------------------------
public interface IDraw {
   void Draw();
}
public class GeometricObject {/*...*/}
public class Circle : GeometricObject {/*...*/}
public class GraphicCircle : Circle, IDraw {
  public void Draw() {
    Console.WriteLine("Der Kreis wird gezeichnet");
  }
}
public class Rectangle : GeometricObject {/*...*/}
public class GraphicRectangle : Rectangle, IDraw {
  public void Draw() {
    Console.WriteLine("Das Rechteck wird gezeichnet");
  }
}

Nun soll ein Array Objekte vom Typ Circle, GraphicCircle, Rectangle und GraphicRectangle verwalten. Die Methode Draw der Schnittstelle IDraw soll auf alle Objekte ausgeführt werden, die diese Schnittstelle veröffentlichen. Dabei kann es sich nur um die Objekte vom Typ der GraphicCircle und GraphicRectangle handeln.

Zur Vermeidung von Fehlern muss in einer Schleife jede Objektreferenz zuerst daraufhin geprüft werden, ob der von ihr beschriebene Typ die Schnittstelle IDraw implementiert. Der Typoperator is, der im Kontext der if-Anweisung eingesetzt wird, erfüllt diese Aufgabe:


if(<Objektreferenz> is <Schnittstelle>) ...

Wir haben diesen Operator bereits im Zusammenhang mit der Typüberprüfung eingesetzt.

In der folgenden Main-Methode wird ein Array mit zehn Elementen vom Typ der allen gemeinsamen Basisklasse GeometricObject deklariert. In einer Schleife wird jedem Array-Element die Referenz auf ein Objekt vom Typ Circle, GraphicCircle, Rectangle oder GraphicRectangle zugewiesen. Die überladene Methode Next der Klasse Random, die uns mit den beiden Argumenten 0 und 4 die Zufallszahlen 0, 1, 2 oder 3 liefert, bestimmt mit dem jeweiligen Rückgabewert für jedes Element den entsprechenden Typ. Ist der Rückgabewert eine 0, wird die Klasse Circle instanziiert, mit 1 die Klasse GraphicCircle, usw.


class Class1 {
  static void Main(string[] args) {
    GeometricObject[] obj = new GeometricObject[10];
    Random rnd = new Random();
    int zufall;
    // Array-Elemente initialisieren
    for(int i = 0; i < obj.Length; i++) {
      zufall = rnd.Next(0, 4);
      switch(zufall) {
        case 0:
          obj[i] = new Circle();
          break;
        case 1:
          obj[i] = new GraphicCircle();
          break;
        case 2:
          obj[i] = new Rectangle();
          break;
        case 3:
          obj[i] = new GraphicRectangle();
          break;
      }
    }
    // Ausgabe an der Konsole
    for(int i = 0; i < obj.Length; i++) {
      if(obj[i] is IDraw) {
        Console.WriteLine(obj[i].GetType());
        ((IDraw)obj[i]).Draw();
        Console.WriteLine("----------------------------");
      }
    }
    Console.ReadLine();
  }
} 

Interessant ist für uns insbesondere die zweite Schleife, in der mit


if(obj[i] is IDraw)

geprüft wird, ob der Typ der aktuellen Referenz die Schnittstelle IDraw implementiert. Im positiven Fall wird das Array-Alement zuerst explizit in den Typ der Schnittstelle konvertiert und darauf die Methode Draw polymorph ausgeführt:


((IDraw)obj[i]).Draw();

Die Ausgabe könnte beispielsweise wie folgt lauten:


CircleApplication.GraphicCircle
Der Kreis wird gezeichnet
---------------------------------------------
CircleApplication.GraphicRectangle
Das Rechteck wird gezeichnet
---------------------------------------------
...

Wie Sie sehen, spielt auch hier die Polymorphie eine tragende Rolle, um die zu einem bestimmten Objekt gehörende Methode aufzurufen.


Galileo Computing

6.10.4 Typumwandlung mit dem »as«-Operatodowntop

Ein Objekt kann mit dem ()-Konvertierungsoperator in eine kompatible Schnittstelle umgewandelt werden. Beispielsweise kann eine Instanz der Klasse GraphicCircle in die Schnittstelle IDraw konvertiert und dann einer Variablen dieses Typs zugewiesen werden:


IDraw id = (IDraw)obj;

C# bietet mit dem as-Operator noch eine weitere Konvertierungsvariante an:


IDraw id = obj as IDraw;

Das Ergebnis ist dasselbe – wenn die angegebene Instanz auch wirklich die Schnittstelle implementiert. Beide Möglichkeiten, der is- und der as-Operator, verhalten sich aber grundsätzlich unterschiedlich, wenn das Objekt die Schnittstelle nicht implementiert. Die Prüfung mit dem is-Operator löst nämlich eine Exception (Ausnahme) aus, während der as-Operator null zurückliefert. Letzterer bietet sich deshalb auch insbesondere dann an, wenn in einem if-Statement eine Schnittstellenuntersuchung vorgenommen werden soll:


if(obj as IDraw != null) ...


Galileo Computing

6.10.5 Abstrakte Klassen vs. Schnittstellen  downtop

Die Notwendigkeit bzw. der Vorteil der Schnittstellenimplementierung ist am Anfang nicht einfach zu verstehen. Bestimmt werden Sie erkannt haben, dass die Schnittstellenimplementierung als eine Ergänzung des Vererbungskonzepts angesehen werden kann. Die Beispiele waren bisher alle sehr einfach gehalten, nun wollen wir uns einem komplexeren Beispiel zuwenden und dabei die Lösung für ein Problem erarbeiten, das zuerst durch eine abstrakte Klasse beschrieben wird und im zweiten Ansatz durch eine Schnittstelle.

Problembeschreibung

Stellen Sie sich vor, Sie möchten einen Algorithmus implementieren, der die Objekte eines Arrays der Größe nach auf- bzw. absteigend sortiert. Der Algorithmus soll sich dabei nur auf bestimmte .NET-Typen beschränken. Eine leichte Aufgabe, werden Sie jetzt vermutlich denken. Aber aus der Einschränkung auf bestimmte Typen resultiert eine verhältnismäßig komplizierte Lösung.

Die Sortierroutine

Am Anfang steht die Überlegung, die Sortierroutine mit dem Bezeichner SortElements in einer eigenen Klasse zu implementieren. Der erste Ansatz könnte dann wie folgt aussehen:


public class ArraySort {
  public static void SortElements(IrgendEinTyp[] arr) {
    // Anweisungen
  }
}

Der Rückgabewert von SortElements ist void, folglich wird das sortierte Array über den Parameter an den Aufrufer zurückgegeben. Der Implementierung werden wir uns gleich widmen, denn zuerst müssen wir uns Gedanken darüber machen, von welchem Typ das übergebene Array sein soll. Im Codefragment ist der Typ noch mit IrgendEinTyp angegeben.

Um die Sortierung auf bestimmte Typen einzuschränken, müssen wir diesen exakt festlegen. Dazu definieren wir eine zweite Klasse, die später als Basisklasse von den Klassen abgeleitet werden muss, deren Instanzen von SortElements sortiert werden sollen. Wir legen damit den Typ des übergebenen Arrays fest, denn nach den Regeln der Objektorientierung gilt, dass ein Objekt einer abgeleiteten Klasse auch gleichzeitig vom Typ seiner Basisklasse ist.


public class SortableObject {
  // Anweisungen
}

Nun können wir den Methodenkopf von SortElements anpassen:


public static void SortElements(SortableObject[] arr)

Damit genügen wir der Forderung, nur bestimmte .NET-Typen sortieren zu können. Unabhängig davon, ob ein Array vom Typ Person, Elefant oder ClassA übergeben wird, wird der Parameter das Array mittels impliziter Konvertierung in Empfang nehmen – vorausgesetzt natürlich, dass die Klassen von SortableObject abgeleitet sind. Widmen wir uns nun der Realisierung der Methode SortElements. Es gibt verschiedene Algorithmen, um Elemente zu sortieren: Bubblesort, Quicksort, Insertionsort – um nur einige zu nennen. Die Bevorzugung eines dieser Sortierverfahren hängt vom Umfang der Daten und vom durchzuführenden Vergleich ab.

Für unser Beispiel habe ich mich für das Bubblesort-Verfahren entschieden. Der Name rührt wohl daher, dass sich die Funktionsweise sehr gut mit den aufsteigenden Luftblasen in einer Flüssigkeit vergleichen lässt. Die Elemente eines Arrays werden in aufsteigender Richtung durchlaufen, und dabei werden immer zwei benachbarte Elemente verglichen. Angenommen, ein Array namens MyArr mit vier Elementen soll der Größe nach sortiert werden, dann würden nacheinander die Elementpaare


MyArr[0] – MyArr[1]
MyArr[1] – MyArr[2]
MyArr[2] – MyArr[3]

verglichen und jedes Paar in die richtige Reihenfolge gebracht. Wenn das Array aufsteigend sortiert werden soll, muss das zweite Element größer als das erste sein. Die Folge ist nach diesen drei Vergleichen, dass das höchstwertige Element – selbst wenn es sich im ursprünglichen Array ganz am Anfang befindet – bis an das Ende des Arrays (MyArr[3]) durchgereicht wird. Die Anzahl der Paarvergleiche entspricht der Bedingung:

Anzahl der Array-Elemente – 1

Dieser Durchlauf wird wiederholt, wobei das bereits richtig einsortierte Element keine Berücksichtigung mehr findet:


MyArr[0] – MyArr[1]
MyArr[1] – MyArr[2]

Nach dem zweiten Durchlauf befindet sich das Element mit dem zweithöchsten Wert an der vorletzten Array-Position. Die Paarvergleiche werden so lange fortgesetzt, bis der Algorithmus mit dem letzten Paarvergleich


MyArr[0] – MyArr[1]

beendet wird.

Das Bubblesort-Sortierverfahren lässt sich am einfachsten mit zwei Schleifen implementieren:

gp  Eine äußere Schleife mit einer Anzahl von Schleifendurchläufen, die der Bedingung Anzahl der Arrayelemente – 1 genügt.
gp  Eine innere Schleife, die den Paarvergleich durchführt und gegebenenfalls die Reihenfolge der benachbarten Elemente vertauscht.

Ausschlaggebend dafür, an welcher Position sich ein Objekt im sortierten Array einreiht, ist der paarweise Objektvergleich. Es stellt sich nun allerdings die Frage, nach welchen Kriterien Objekte vom Typ SortableObject verglichen werden sollen. Die statische Methode SortElements kann darüber keine Entscheidung treffen, da sie die typspezifischen Vergleichskriterien nicht kennt. Konsequenterweise muss der Vergleich in den von SortableObject abgeleiteten Klassen erfolgen. Dazu wird den ableitenden Klassen eine Methode vorgeschrieben, die als Ergebnis des Vergleichs zweier typgleicher Objekte einen booleschen Wert liefert. Wir nennen diese Methode CompareTo.

Damit jede Klasse, die SortableObject ableitet, die Methode CompareTo nach eigenen Maßstäben implementiert, wird CompareTo in der Klasse SortableObject abstrakt definiert. Damit sieht die endgültige Klassendefinition wie folgt aus:


public abstract class SortableObject {
  public abstract bool CompareTo(SortableObject obj);
}

Der Rückgabewert soll true sein, wenn das Objekt, auf dem die Methode CompareTo aufgerufen wird, größer ist als das Objekt, das dem Parameter übergeben wird. In allen anderen Fällen sei der Rückgabewert false. Wie und nach welchen Gesichtspunkten der Vergleich erfolgt, entscheidet die Klasse, welche die abstrakte Methode CompareTo überschreibt. Natürlich können die booleschen Rückgabewerte auch vertauscht werden. Dann werden die Array-Elemente jedoch nicht auf-, sondern absteigend sortiert.

Mit diesen Vorgaben kann nun die Methode SortElements vollständig implementiert werden:


public class ArraySort {
  public static void SortElements(SortableObject[] arr) {
    // n = Anzahl der Elemente
    int n = arr.Length;
    // Temp = temporäre Variable
    SortableObject Temp;
    for(int i = n – 1; i >= 1; i--) {
      for(int k = 0; k <= i-1; k++) {
        if(arr[k].CompareTo(arr[k + 1])) {
          Temp = arr[k];
          arr[k] = arr[k + 1];
          arr[k + 1] = Temp;
        }
      }
    }
  }
}

Resümieren wir an dieser Stelle, denn wir haben bereits alle Anforderungen erfüllt. Wir haben die abstrakte Klasse SortableObject entwickelt, welche die Methode CompareTo bereitstellt, um zwei Objekte miteinander zu vergleichen. Per Definition kann die Methode CompareTo nur Objekte vergleichen, deren Klassen die abstrakte Klasse SortableObject ableiten.

In der Klasse ArraySort ist eine Methode implementiert, die in der Lage ist, ein Array von SortableObject-Objekten der Größe nach zu sortieren. Aber warum müssen es gerade Objekte dieses Typs sein, warum nicht andere, beliebige Objekte, von denen beispielsweise einfach zwei Längenmaße miteinander verglichen werden? Die Antwort liefert ein Blick in die Implementierung der Sortierroutine SortElements. Der Entwickler der Klasse ArraySort kannte die abstrakte Klasse SortableObject. Er wusste, dass Klassen, welche die Klasse SortableObject ableiten, die Methode CompareTo bereitstellen. Auf diese Kenntnis wird in der Sortierroutine zurückgegriffen, wenn die CompareTo-Methode auf ein Objekt aufgerufen wird.

ArraySort und SortableObject seien in einer Klassenbibliothek implementiert.

Die ableitende Klasse

Versetzen wir uns in die Lage eines Benutzers, der eine Klasse namens LngNumber entwickelt, die unter anderem ein Feld vom Typ long bereitstellt. Dieser Benutzer möchte sicherstellen, dass ein Objekt-Array vom Typ LngNumber der Größe nach sortiert werden kann.

Um sich die Mühe einer eigenen Implementierung zu sparen, recherchiert er in diversen Dokumentationen und stößt auf die Klasse ArraySort mit ihrer Methode SortElements, welche die Lösung seines Problems darstellt. Da beide Klassen voneinander abhängig sind, werden sich beide vermutlich sogar in derselben Klassenbibliothek befinden. Der Dokumentation entnimmt unser fiktiver Benutzer außerdem, dass er die Klasse ArraySort ableiten und deren abstrakte Methode CompareTo implementieren muss. Das Ergebnis könnte in den für uns entscheidenden Punkten wie folgt aussehen:


public class LngNumber : SortableObject {
  private long lngValue;
  // Konstruktor
  public LngNumber(long lng) {
    lngValue = lng;
  }
  // Eigenschaft
  public long Value {
    get {return lngValue;}
    set {lngValue = value;}
  }
  // überschriebene Instanzmethode
  public override bool CompareTo(SortableObject b) {
    if(this.lngValue < ((LngNumber)b).lngValue)
      return true;
    return false;
  }
}

Die Testanwendung

Was uns jetzt noch bleibt, ist die Bestätigung unserer Überlegungen durch eine Testanwendung:


// ----------------------------------------------------------
// Beispiel: ...\Kapitel 6\Sortierroutine1
// ----------------------------------------------------------
class Program {
  static void Main(string[] args) {
    LngNumber[] arr = new LngNumber[5];
    arr[0] = new LngNumber(8);
    arr[1] = new LngNumber(6);
    arr[2] = new LngNumber(34);
    arr[3] = new LngNumber(232);
    arr[4] = new LngNumber(2);
    // Aufruf der statischen Methode SortElements unter  
    // Übergabe des Objekt-Arrays
    ArraySort.SortElements(arr);
    // Ausgabe an der Konsole
    for(int i = 0; i <= 4; i++) {
      Console.Write("Element[" + i + "]");
      Console.WriteLine(" = " + arr[i].Value);
    }
    Console.ReadLine();
  }
}

Zunächst wird ein Array aus fünf Elementen vom Typ LngNumber deklariert, die im zweiten Schritt unter Übergabe der Initialisierungswerte an den Konstruktor konkretisiert werden. Die Array-Elemente liegen zunächst in unsortierter Reihenfolge vor und werden mit der Anweisung


ArraySort.SortElements(arr);

in die richtige Reihenfolge gebracht. Die Ausgabe an der Konsole wird für die Elemente des Arrays arr lauten:


Element[0] = 2
Element[1] = 6
Element[2] = 8
Element[3] = 34
Element[4] = 232

Sie sehen, dass die Sortierroutine ihre Aufgabe einwandfrei erledigt. Wenigstens haben sich unsere Mühen gelohnt, wenn der Weg auch ein wenig steinig war.

Die Lösung mit einer Schnittstellendefinition

Der Code des Beispiels aus dem vorhergehenden Abschnitt funktioniert tadellos. Aber ihm haftet ein wesentliches Problem an, das sehr häufig auftritt, wenn abstrakte Basisklassen abgeleitet werden: Die Common Language Runtime unterstützt keine Mehrfachvererbung, sondern erlaubt nur eine Basisklasse. Solange die Klasse LngNumber nicht aus einer anderen Basisklasse abgeleitet wird, ist der oben gezeigte Lösungsansatz akzeptabel. Sobald aber eine weitere Basisklasse ins Rampenlicht rückt, muss ein anderer Weg beschritten werden.

Genau an dieser Stelle greift das Konzept der Schnittstellen. Denn anstatt eine abstrakte Klasse zu beerben, werden die abstrakten Methoden über eine Schnittstelle offen gelegt. Damit wird ein Großteil der Funktionalität der Mehrfachvererbung wiedererlangt, ohne die damit verbundenen Nachteile in Kauf nehmen zu müssen. Da eine Klasse beliebig viele Schnittstellen implementieren darf, kann sie auch um die unterschiedlichsten Verhaltensweisen erweitert werden.

Damit wird aus der abstrakten Klasse Sortable eine Schnittstellendefinition wie im Folgenden dargestellt:


// ----------------------------------------------------------
// Beispiel: ...\Kapitel 6\Sortierroutine2
// ----------------------------------------------------------
public interface ISortableObject {
  bool CompareTo(ISortableObject a);
}

Konventionsgemäß ergänzen wir den Schnittstellenbezeichner um das Präfix »I«. Die Änderung einer abstrakten Klasse in eine Schnittstelle wirkt sich weder auf die Definition der Klasse ArraySort noch auf die der Methode SortElements aus. Allerdings müssen die Klasse LngNumber und die aus der Schnittstelle übernommenen Methoden an die Schnittstellenimplementierung angepasst werden. Während die Ableitung einer abstrakten Klasse das Überschreiben der abstrakten Methoden mit dem Schlüsselwort override erforderlich macht, ist dieses bei der Implementierung der Schnittstellenmember in der implementierenden Klasse nicht zulässig.

Der folgende Codeausschnitt gibt die notwendigen Änderungen wieder.


class LngNumber : ISortableObject {
  private long lngValue;
  // Konstruktor
  public LngNumber(long lng) {
    lngValue = lng;
  }
  public long Value {
    get {return lngValue;}
    set {lngValue = value;}
  }
  // aus der SortableObject-Schnittstelle übernommene Methode
  public bool CompareTo(ISortableObject b) {
    if(this.lngValue < ((LngNumber)b).lngValue)
      return true;
    return false;
  }
}

Damit ist die ursprünglich abstrakte Klasse durch eine Schnittstelle ersetzt, und der Code wird in gleicher Weise zum Ziel führen. Nicht anzuzweifeln ist die durch die Schnittstellendefinition gewonnene Flexibilität im Vergleich zur abstrakten Klasse, da eine Schnittstelle das möglicherweise unumgängliche Ableiten einer Basisklasse nicht blockiert. Daher sollten Schnittstellen immer dann bevorzugt eingesetzt werden, wenn die Implementierungsvererbung nicht unbedingt notwendig ist.


Galileo Computing

6.10.6 Zusammenfassung  toptop

gp  Schnittstellen sind die konsequente Fortsetzung der Idee einer abstrakten Klasse. Schnittstellen bieten einer Klasse ihre Dienste in Form von abstrakten Membern an, und der Nutzer verpflichtet sich, diese zu implementieren.
gp  Interfaces können nicht instanziiert werden, weil sie nur Verhaltensweisen festlegen, jedoch keine Daten bereitstellen.
gp  Eine Schnittstelle wird mit dem Schlüsselwort interface und dem sich anschließenden Bezeichner definiert. Daran folgt in geschweiften Klammern ein Block abstrakter Definitionen. Konventionsgemäß beginnt der Bezeichner einer Schnittstelle mit »I«.
gp  Im Gegensatz zu einer abstrakten Klasse enthalten Schnittstellen ausschließlich abstrakte Methoden, Eigenschaften oder Indexer.
gp  Eine Klasse kann beliebig viele Schnittstellen implementieren. Implementiert eine Basisklasse eine Schnittstelle, wird die Schnittstelle an alle von dieser Basisklasse abgeleiteten Subklassen weitervererbt.
gp  Ein aus einer Schnittstelle übernommenes Mitglied darf nur public sein. Es ist zulässig, in einer Klasse ein Schnittstellenmitglied abstract oder virtual zu implementieren, es darf jedoch nicht static oder const sein.
gp  Da eine Klasse beliebig viele Schnittstellen implementieren darf, könnte es zu Mehrdeutigkeiten kommen, wenn von zwei oder mehr Schnittstellen eine gleichnamige Methode bereitgestellt wird. Mit der expliziten Implementierung kann dennoch auf jede einzelne Methode zugegriffen werden. Es darf dabei weder ein Zugriffsmodifizierer noch einer der Modifikatoren abstract, virtual, const oder static in der Signatur angegeben werden.
gp  Der is-Operator bietet die Möglichkeit, eine Klasse daraufhin zu untersuchen, ob sie eine bestimmte Schnittstelle implementiert.
gp  Mit dem as-Operator lässt sich einerseits eine Typkonvertierung durchführen und andererseits auch überprüfen, ob ein Objekt eine bestimmte Schnittstelle unterstützt. Der Rückgabewert ist null, wenn das Objekt die angegebene Schnittstelle nicht implementiert.
 << zurück
  
  Zum Katalog
Zum Katalog: Visual C# 2005
Visual C# 2005
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Fortgeschrittene Programmierung mit Visual C# 2005






 Fortgeschrittene
 Programmierung
 mit Visual C# 2005


Zum Katalog: Einstieg in Visual C# 2005






 Einstieg in
 Visual C# 2005


Zum Katalog: Einstieg in Visual Basic 2005






 Einstieg in
 Visual Basic 2005


Zum Katalog: Visual Basic 2005






 Visual Basic 2005


Zum Katalog: Java ist auch eine Insel






 Java ist auch eine
 Insel


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo








Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de